route.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import { getServerSideConfig } from "@/app/config/server";
  2. import {
  3. TENCENT_BASE_URL,
  4. ApiPath,
  5. ModelProvider,
  6. ServiceProvider,
  7. Tencent,
  8. } from "@/app/constant";
  9. import { prettyObject } from "@/app/utils/format";
  10. import { NextRequest, NextResponse } from "next/server";
  11. import { auth } from "@/app/api/auth";
  12. import { isModelAvailableInServer } from "@/app/utils/model";
  13. import CryptoJS from "crypto-js";
  14. import mapKeys from "lodash-es/mapKeys";
  15. import mapValues from "lodash-es/mapValues";
  16. import isArray from "lodash-es/isArray";
  17. import isObject from "lodash-es/isObject";
  18. const serverConfig = getServerSideConfig();
  19. async function handle(
  20. req: NextRequest,
  21. { params }: { params: { path: string[] } },
  22. ) {
  23. console.log("[Tencent Route] params ", params);
  24. if (req.method === "OPTIONS") {
  25. return NextResponse.json({ body: "OK" }, { status: 200 });
  26. }
  27. const authResult = auth(req, ModelProvider.Hunyuan);
  28. if (authResult.error) {
  29. return NextResponse.json(authResult, {
  30. status: 401,
  31. });
  32. }
  33. try {
  34. const response = await request(req);
  35. return response;
  36. } catch (e) {
  37. console.error("[Tencent] ", e);
  38. return NextResponse.json(prettyObject(e));
  39. }
  40. }
  41. export const GET = handle;
  42. export const POST = handle;
  43. export const runtime = "edge";
  44. export const preferredRegion = [
  45. "arn1",
  46. "bom1",
  47. "cdg1",
  48. "cle1",
  49. "cpt1",
  50. "dub1",
  51. "fra1",
  52. "gru1",
  53. "hnd1",
  54. "iad1",
  55. "icn1",
  56. "kix1",
  57. "lhr1",
  58. "pdx1",
  59. "sfo1",
  60. "sin1",
  61. "syd1",
  62. ];
  63. async function request(req: NextRequest) {
  64. const controller = new AbortController();
  65. // tencent just use base url or just remove the path
  66. let path = `${req.nextUrl.pathname}`.replaceAll(
  67. ApiPath.Tencent + "/" + Tencent.ChatPath,
  68. "",
  69. );
  70. let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
  71. if (!baseUrl.startsWith("http")) {
  72. baseUrl = `https://${baseUrl}`;
  73. }
  74. if (baseUrl.endsWith("/")) {
  75. baseUrl = baseUrl.slice(0, -1);
  76. }
  77. console.log("[Proxy] ", path);
  78. console.log("[Base Url]", baseUrl);
  79. const timeoutId = setTimeout(
  80. () => {
  81. controller.abort();
  82. },
  83. 10 * 60 * 1000,
  84. );
  85. const fetchUrl = `${baseUrl}${path}`;
  86. let body = null;
  87. if (req.body) {
  88. const bodyText = await req.text();
  89. console.log(
  90. "Dogtiti ~ request ~ capitalizeKeys(JSON.parse(bodyText):",
  91. capitalizeKeys(JSON.parse(bodyText)),
  92. );
  93. body = JSON.stringify(capitalizeKeys(JSON.parse(bodyText)));
  94. }
  95. const fetchOptions: RequestInit = {
  96. headers: {
  97. ...getHeader(body),
  98. },
  99. method: req.method,
  100. body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME
  101. redirect: "manual",
  102. // @ts-ignore
  103. duplex: "half",
  104. signal: controller.signal,
  105. };
  106. // #1815 try to refuse some request to some models
  107. if (serverConfig.customModels && req.body) {
  108. try {
  109. const clonedBody = await req.text();
  110. fetchOptions.body = clonedBody;
  111. const jsonBody = JSON.parse(clonedBody) as { model?: string };
  112. // not undefined and is false
  113. if (
  114. isModelAvailableInServer(
  115. serverConfig.customModels,
  116. jsonBody?.model as string,
  117. ServiceProvider.Tencent as string,
  118. )
  119. ) {
  120. return NextResponse.json(
  121. {
  122. error: true,
  123. message: `you are not allowed to use ${jsonBody?.model} model`,
  124. },
  125. {
  126. status: 403,
  127. },
  128. );
  129. }
  130. } catch (e) {
  131. console.error(`[Tencent] filter`, e);
  132. }
  133. }
  134. console.log("[Tencent request]", fetchOptions.headers, req.method);
  135. try {
  136. const res = await fetch(fetchUrl, fetchOptions);
  137. console.log("[Tencent response]", res.status, " ", res.headers, res.url);
  138. // to prevent browser prompt for credentials
  139. const newHeaders = new Headers(res.headers);
  140. newHeaders.delete("www-authenticate");
  141. // to disable nginx buffering
  142. newHeaders.set("X-Accel-Buffering", "no");
  143. return new Response(res.body, {
  144. status: res.status,
  145. statusText: res.statusText,
  146. headers: newHeaders,
  147. });
  148. } finally {
  149. clearTimeout(timeoutId);
  150. }
  151. }
  152. function capitalizeKeys(obj: any): any {
  153. if (isArray(obj)) {
  154. return obj.map(capitalizeKeys);
  155. } else if (isObject(obj)) {
  156. return mapValues(
  157. mapKeys(
  158. obj,
  159. (value: any, key: string) => key.charAt(0).toUpperCase() + key.slice(1),
  160. ),
  161. capitalizeKeys,
  162. );
  163. } else {
  164. return obj;
  165. }
  166. }
  167. // 使用 SHA-256 和 secret 进行 HMAC 加密
  168. function sha256(message: any, secret = "", encoding = "hex") {
  169. const hmac = CryptoJS.HmacSHA256(message, secret);
  170. if (encoding === "hex") {
  171. return hmac.toString(CryptoJS.enc.Hex);
  172. } else if (encoding === "base64") {
  173. return hmac.toString(CryptoJS.enc.Base64);
  174. } else {
  175. return hmac.toString();
  176. }
  177. }
  178. // 使用 SHA-256 进行哈希
  179. function getHash(message: any, encoding = "hex") {
  180. const hash = CryptoJS.SHA256(message);
  181. if (encoding === "hex") {
  182. return hash.toString(CryptoJS.enc.Hex);
  183. } else if (encoding === "base64") {
  184. return hash.toString(CryptoJS.enc.Base64);
  185. } else {
  186. return hash.toString();
  187. }
  188. }
  189. function getDate(timestamp: number) {
  190. const date = new Date(timestamp * 1000);
  191. const year = date.getUTCFullYear();
  192. const month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
  193. const day = ("0" + date.getUTCDate()).slice(-2);
  194. return `${year}-${month}-${day}`;
  195. }
  196. function getHeader(payload: any) {
  197. // https://cloud.tencent.com/document/api/1729/105701
  198. // 密钥参数
  199. const SECRET_ID = serverConfig.tencentSecretId;
  200. const SECRET_KEY = serverConfig.tencentSecretKey;
  201. const endpoint = "hunyuan.tencentcloudapi.com";
  202. const service = "hunyuan";
  203. const region = ""; // optional
  204. const action = "ChatCompletions";
  205. const version = "2023-09-01";
  206. const timestamp = Math.floor(Date.now() / 1000);
  207. //时间处理, 获取世界时间日期
  208. const date = getDate(timestamp);
  209. // ************* 步骤 1:拼接规范请求串 *************
  210. const hashedRequestPayload = getHash(payload);
  211. const httpRequestMethod = "POST";
  212. const canonicalUri = "/";
  213. const canonicalQueryString = "";
  214. const canonicalHeaders =
  215. "content-type:application/json; charset=utf-8\n" +
  216. "host:" +
  217. endpoint +
  218. "\n" +
  219. "x-tc-action:" +
  220. action.toLowerCase() +
  221. "\n";
  222. const signedHeaders = "content-type;host;x-tc-action";
  223. const canonicalRequest =
  224. httpRequestMethod +
  225. "\n" +
  226. canonicalUri +
  227. "\n" +
  228. canonicalQueryString +
  229. "\n" +
  230. canonicalHeaders +
  231. "\n" +
  232. signedHeaders +
  233. "\n" +
  234. hashedRequestPayload;
  235. // ************* 步骤 2:拼接待签名字符串 *************
  236. const algorithm = "TC3-HMAC-SHA256";
  237. const hashedCanonicalRequest = getHash(canonicalRequest);
  238. const credentialScope = date + "/" + service + "/" + "tc3_request";
  239. const stringToSign =
  240. algorithm +
  241. "\n" +
  242. timestamp +
  243. "\n" +
  244. credentialScope +
  245. "\n" +
  246. hashedCanonicalRequest;
  247. // ************* 步骤 3:计算签名 *************
  248. const kDate = sha256(date, "TC3" + SECRET_KEY);
  249. const kService = sha256(service, kDate);
  250. const kSigning = sha256("tc3_request", kService);
  251. const signature = sha256(stringToSign, kSigning, "hex");
  252. // ************* 步骤 4:拼接 Authorization *************
  253. const authorization =
  254. algorithm +
  255. " " +
  256. "Credential=" +
  257. SECRET_ID +
  258. "/" +
  259. credentialScope +
  260. ", " +
  261. "SignedHeaders=" +
  262. signedHeaders +
  263. ", " +
  264. "Signature=" +
  265. signature;
  266. return {
  267. Authorization: authorization,
  268. "Content-Type": "application/json; charset=utf-8",
  269. Host: endpoint,
  270. "X-TC-Action": action,
  271. "X-TC-Timestamp": timestamp.toString(),
  272. "X-TC-Version": version,
  273. "X-TC-Region": region,
  274. };
  275. }